package pulpfizz.pulp.body;

import java.util.ArrayList;
import java.util.HashMap;

import org.jbox2d.collision.AABB;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;

import pulpcore.Stage;
import pulpcore.animation.Property;
import pulpcore.math.CoreMath;
import pulpcore.sprite.Group;
import pulpcore.sprite.Sprite;
import pulpfizz.physics.NewWorld;

/**
 * The PhysicsLayer Group holds the JBox2d physics world. It manages the drawing
 * and scaling of the BodySprite objects it maintains.
 * 
 * 
 * The core of this class is the transformation matrix it sets up between JBox2D
 * World and Layer coordinates.
 */
public class PhysicsLayer extends Group
{
	NewWorld world;

	/**
	 * The amount by which to scale up the physics. With a scaling value of 10,
	 * a 1-meter physics object will take up 10 pixels.
	 */
	float scaling = 20;

	float physicsWidth;
	float physicsHeight;
	
	HashMap<Body,ArrayList<BodySpriteOld>> bodySprites;
	
	// Minimum frame rate to allow physics to simulate at
	final int minFrameRate = 45;

	public PhysicsLayer(double scaling)
	{
		this(scaling, 0, 0, 100, 100);
	}

	public PhysicsLayer(double scaling, double x, double y, double width,
			double height)
	{
		/*
		 * We need to call the super constructor with a (0,0) width and height... otherwise,
		 * the Group class tries to keep track of the "natural" width and height, and then if
		 * we later scale the PhysicsLayer, things will look distorted.
		 * 
		 * The solution is to call the super constructor with (0,0) with and height, and subsequently set
		 * the width and height manually.
		 */
		super(x, y, 0, 0); // Call the super constructor with zero width and height.
		this.scaling = (float) scaling;
		this.width.addListener(this);
		this.height.addListener(this);
		this.width.set(width); // Set the width and height manually.
		this.height.set(height);
		this.enabled.set(false); //Disable mouse input (does this actually help performance?)
		bodySprites = new HashMap<Body,ArrayList<BodySpriteOld>>();
	}
	
	@Override
	public void removeAll() {
		super.removeAll();
		bodySprites.clear();
	}
	
	/** Add a BodySprite. */
	public void add(BodySpriteOld bs) {
		super.add(bs);
		ArrayList<BodySpriteOld> list = bodySprites.get(bs.getBody());
		if (list == null) {
			ArrayList<BodySpriteOld> toAdd = new ArrayList<BodySpriteOld>();
			toAdd.add(bs);
			bodySprites.put(bs.getBody(), toAdd);
		} else {
			list.add(bs);
		}
	}
	
	/** Remove a BodySprite. */
	public void remove(BodySpriteOld bs) {
		bodySprites.remove(bs.getBody());
		ArrayList<BodySpriteOld> list = bodySprites.get(bs.getBody());
		if (list != null) {
			list.remove(bs);
			if (list.size() == 0) bodySprites.remove(bs.getBody());
		}		
		super.remove(bs);
	}

	/** Remove all sprites associated with a given body. */
	public void removeSpritesByBody(Body body) {
		//BodySprite s = (BodySprite)(bodySprites.get(body));
		ArrayList<BodySpriteOld> list = bodySprites.get(body);
		if (list==null) return; //can happen if we attempt multiple removals
		for (BodySpriteOld s:list) {
			remove(s);
		}
		bodySprites.remove(body);
	}
	
	/**
	 * Return the first sprite found that matches a given body.
	 * It's better to use getSpritesByBody() because that returns
	 * the entire sprite list associated with a body, but in many
	 * cases there is a one-to-one sprite-to-body mapping, and this
	 * functions is convenient where that is the case.
	 * @param body
	 * @return
	 */
	public BodySpriteOld getSpriteByBody(Body body) {
		ArrayList<BodySpriteOld> list = bodySprites.get(body);
		if (list==null) return null;
		BodySpriteOld s = list.get(0);
		return s;
	}
	
	/**
	 * Return the entire list of sprites associated with a body.
	 * If you are sure that your body only has one sprite associated
	 * with it, you may use getSpriteByBody() instead.
	 * @param body
	 * @return
	 */
	public ArrayList<BodySpriteOld> getSpritesByBody(Body body) {
		return bodySprites.get(body);
	}

	public void fitToStage()
	{
		setSize(Stage.getWidth(), Stage.getHeight());
		double offsetX = CoreMath.toDouble(getAnchorX());
		double offsetY = CoreMath.toDouble(getAnchorY());
		setLocation(offsetX, offsetY);
	}

	/**
	 * Resets the world by destroying any existing object and creating a new
	 * one.
	 */
	public void resetWorld()
	{
		if (world != null)
			world.destroy();
		physicsWidth = (float) width.get() / scaling;
		physicsHeight = (float) height.get() / scaling;
		AABB bounds = new AABB(new Vec2(-physicsWidth / 2, -physicsHeight / 2),
							   new Vec2( physicsWidth / 2,  physicsHeight / 2));
		world = new NewWorld(bounds);
	}
	
	/**
	 * Set the physical dimensions of the world bounding box, in meters.
	 * Always overshoot here - making this slightly too big will never cause
	 * physics problems, as long as the world is less than ~10 km or so (even
	 * then, performance will just suffer a bit, nothing should break),
	 * but making it too small will cause bodies to freeze if they pass
	 * the boundary.  If the size is way too large, the getPhysicsX/Y functions may return
	 * garbage values due to overflow - a workaround is planned for this
	 * at some point in the future.
	 * <BR><BR>
	 * Warning: this will clear the World and all bodies,
	 * creating a brand new World, so make sure you don't
	 * hold on to any refs to the old one, they will be stale.
	 * @param newWidth
	 * @param newHeight
	 */
	public void setPhysicsDimensions(float newWidth, float newHeight) {
		physicsWidth = newWidth;
		physicsHeight = newHeight;
		width.set(newWidth * scaling);
		height.set(newHeight * scaling);
		resetWorld();
	}

	/**
	 * Changes the scaling of the physics.
	 * 
	 * Warning: this resets the current World object, meaning if you add any
	 * bodies to the world BEFORE calling this method, they will be wiped clean!
	 */
	public void setScaling(float scaling)
	{
		this.scaling = scaling;
		resetWorld();
	}

	public float getScaling()
	{
		return scaling;
	}

    public void setGravity(Vec2 gravity) {
        this.world.setGravity(gravity);
    }

    /**
	 * Listens for any resizing of the physics world.
	 */
	@Override
	public void propertyChange(Property property)
	{
		super.propertyChange(property);
		if (property == width || property == height)
		{
			resetWorld();
		}
	}

	@Override
	public void update(int elapsedTime)
	{
		super.update(elapsedTime);

		// supposedly it's better not to allow variable timesteps with the physics
		//  in order to keep things determinate, or so says erin catto
		//		world.update(elapsedTime);
		//world.update(1000 / 30);
        int fr = Stage.getFrameRate();
        if (fr < minFrameRate) fr = minFrameRate;
        world.update( (int)(1000.0f / fr) );
	}

	float onePixel = 1;

	public float onePixel()
	{
		return onePixel;
	}

	public NewWorld getWorld()
	{
		return world;
	}
	
	public Vec2 getCanvasVector(Vec2 v) {
		float myX = v.x;
		myX /= getPhysicsWidth() / width.get();
		myX += width.get() / 2;
		float myY = v.y;
		myY /= -getPhysicsHeight() / height.get();
		myY += height.get() / 2;
		return new Vec2(myX,myY);
	}

	/**
	 * Gets the x value of a Body in Layer coordinates.
	 */
	public double getCanvasX(Body b)
	{
		float physX = b.getPosition().x;
		physX /= getPhysicsWidth() / width.get();
		physX += width.get() / 2;
		return physX;
	}

	/**
	 * Gets the y value of a Body in Layer coordinates.
	 */
	public double getCanvasY(Body b)
	{
		float physY = b.getPosition().y;
		physY /= -getPhysicsHeight() / height.get();
		physY += height.get() / 2;
		return physY;
		//		return getViewY(physX, physY);
	}
	
	public double getCanvasCMX(Body b) {
		float physX = b.getWorldCenter().x;
		physX /= getPhysicsWidth() / width.get();
		physX += width.get() / 2;
		return physX;
	}
	
	public double getCanvasCMY(Body b) {
		float physY = b.getWorldCenter().y;
		physY /= -getPhysicsHeight() / height.get();
		physY += height.get() / 2;
		return physY;
	}

	/**
	 * Gets the angle of a Body.
	 */
	public double getCanvasAngle(Body b)
	{
		return -b.getAngle();
	}

	public void canvasToPhysicsLocal(Vec2 canvasPoint)
	{
		double physicsX = getPhysicsX(canvasPoint.x,canvasPoint.y);
		double physicsY = getPhysicsY(canvasPoint.x,canvasPoint.y);
		canvasPoint.set((float)physicsX,(float)physicsY);
	}
	
	public Vec2 canvasToPhysics(Vec2 canvasPoint)
	{
		double physicsX = getPhysicsX(canvasPoint.x,canvasPoint.y);
		double physicsY = getPhysicsY(canvasPoint.x,canvasPoint.y);
		return new Vec2((float)physicsX,(float)physicsY);
	}
	
	public double getPhysicsX(double x, double y)
	{
		//FIXME: For certain scaling factors, getLocalX returns
		//large negative numbers, suggesting some sort of overflow
		//or fixed point bug.  See if there's a way around this,
		//otherwise this function is pretty useless when zoomed
		//in very far...
		double localX = getLocalX(x, y);
		localX -= width.get() / 2;
		localX *= getPhysicsWidth() / width.get();
		return localX;
	}

	public double getPhysicsY(double x, double y)
	{
		double localY = getLocalY(
			x, y);
		localY -= height.get() / 2;
		localY *= -getPhysicsHeight() / height.get();
		return localY;
	}

	public final float getPhysicsWidth()
	{
		return physicsWidth;
	}

	public final float getPhysicsHeight()
	{
		return physicsHeight;
	}


}
